#!/bin/bash
#
# Copyright 2015-2017 Adrian DC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# === Git Branch Pusher ===
function gitbranchpusher()
{
  # Usage: gitbranchpusher [y/n] [remote_url] [branch] (Push to project specific branch)

  # Variables
  local project_dir=${PWD};
  local repo_dir=${PWD};
  local param_send=${1};
  local remote=${2};
  local project_branch=${3};
  local push_command;

  # Check project
  if [ ! -d "${project_dir}/.git" ]; then
    echo '';
    echo ' Current project was not found...';
    echo '';
    return;
  fi;

  # Find repository
  while [[ "${repo_dir}" != '/' && ! -d "${repo_dir}/.repo" ]]; do
    repo_dir=$(readlink -f "${repo_dir}/..");
  done;
  if [ ! -d "${repo_dir}/.repo" ]; then
    echo '';
    echo ' Repository was not found';
    echo '';
    return;
  fi;

  # Project target
  project_dir=${project_dir#${repo_dir}/};
  if [ -z "${project_branch}" ]; then
    project_branch=${project_dir//\//_};
  fi;

  # Target remote
  if [ -z "${remote}" ] && [ -f "$(gettop)/.repo/local_manifests/roomservice.xml" ]; then
    remote=$(grep "Project: '" "$(gettop)/.repo/local_manifests/roomservice.xml" | sed "s/.* '\(.*\)' .*/\1/");
    if [ ! -z "${remote}" ]; then
      remote=${remote#* };
      remote=${remote% *};
    fi;
  fi;

  # Failed to detect target remote
  if [ -z "${remote}" ]; then
    echo '';
    echo -e " \e[1;31mgitbranchpusher: \e[1;37mProject url not provided by roomservice.xml\e[0m";
    echo '';
    return;
  fi;

  # Status information
  echo '';
  echo -e " \e[1;32mProject directory:\e[0m ${project_dir}";
  echo -e " \e[1;33mRepository path:\e[0m ${repo_dir}";
  echo -e " \e[1;37mTarget remote:\e[0m ${remote}";
  echo '';

  # Fetch upstream remote
  git fetch "${remote}" "${project_branch}";

  # Present diff statistics
  echo '';
  echo -e ' \e[1;31mSources content differences:\e[0m';
  git --no-pager diff --stat HEAD FETCH_HEAD;

  # Present commits counts
  echo '';
  echo -en ' \e[1;32mNew changes - missing: ';
  git rev-list --left-right --count FETCH_HEAD...HEAD;
  echo -e '\e[0m';

  # Confirmation
  push_command="git push -f ${remote} HEAD:refs/heads/${project_branch}";
  echo -en "  \e[1;33m> gitbranchpusher: ${push_command} [y/N] ?\e[0m ";
  notify-send "gitbranchpusher: Push ${project_dir} ?" 2> /dev/null;
  if [ -z "${param_send}" ]; then
    read -r -t 20 key;
  else
    key=y;
    echo "${key}";
  fi;

  # Upload to remote
  echo '';
  if [ "${key}" = 'y' ] || [ "${key}" = 'Y' ]; then
    echo '';
    ${push_command};
  elif [ "${key}" = 'n' ] || [ "${key}" = 'N' ]; then
    echo '  < Upload ignored. Consider uploading later';
  else
    echo '  < Upload ignored after timeout... Consider uploading later';
  fi;
  echo '';
}

# === Android Project Paths ===
function androidprojectpaths()
{
  # Usage
  if [ -z "${1}" ] || [ -z "${2}" ]; then
    echo '';
    echo ' Usage: androidprojectpaths <owner> <project_name> (Android project remote paths list)';
    echo '';
    return;
  fi;

  # Variables
  local paths_regex;

  # Android project remote paths list
  paths_regex=$(git ls-remote --heads "https://github.com/${1}/${2}.git" \
                  2> /dev/null \
              | sed 's/.*refs\/heads\///' \
              | grep -v 'local_manifests' \
              | sed 's/^\(.*\)$/\^\1\$\\\|/g' \
              | sed 's/_/[\/_]/g' \
              | tr -d '\n');
  paths_regex=${paths_regex%\\\|};

  # Detect available project paths
  if [ ! -z "${paths_regex}" ]; then
    grep --color=never "${paths_regex}" "$(gettop)/.repo/project.list";
  fi;
}

# === Android Project Patcher ===
function androidprojectpatcher()
{
  # Usage
  if [ -z "${1}" ]; then
    echo '';
    echo ' Usage: androidprojectpatcher <owner> <project_name> <email> [specific_path] (Android project patcher)';
    echo '';
    return 1;
  fi;

  # Variables
  local project_owner=${1};
  local project_name=${2};
  local project_email=${3};
  local project_specific_inputs=("${@:4}");
  local android_top;
  local project_count;
  local project_index;
  local project_paths=();
  local currentdir=${PWD};

  # ==========================================================
  # Load Android tools
  if [ -z "$(type -t croot)" ]; then
    while [ ! -e './build/envsetup.sh' ]; do
      cd ../;
    done;
    source ./build/envsetup.sh;
  fi;
  cd "${currentdir}";

  # ==========================================================
  # Automated key shortcut
  local key_input=;
  local key_automated_patch=;
  local key_automated_push=;
  if [[ "${project_specific_inputs[*]}" == 'a' ]] || \
      [[ "${project_specific_inputs[*]}" == 'y' ]] || \
      [[ "${project_specific_inputs[*]}" == 'n' ]]; then
    key_input=${project_specific_inputs[*]};
    project_specific_inputs=();
  fi;

  # ==========================================================
  # Detect 'sync' input
  if [ "${project_specific_inputs[*]}" = 'sync' ]; then
    androidprojectsync "${project_owner}" "${project_name}";
    return;
  fi;

  # ==========================================================
  # Detect 'ungraft' input
  if [ "${project_specific_inputs[*]}" = 'ungraft' ]; then
    androidprojectungraft "${project_owner}" "${project_name}";
    return;
  fi;

  # ==========================================================
  # Detect 'unshallow' input
  if [ "${project_specific_inputs[*]}" = 'unshallow' ]; then
    androidprojectunshallow "${project_owner}" "${project_name}";
    return;
  fi;

  # ==========================================================
  # Extract specific projects paths
  if [ ! -z "${project_specific_inputs[*]}" ]; then
    android_top=$(gettop);
    for path in "${project_specific_inputs[@]}"; do
      path=$(readlink -f "${path}");
      project_paths+=("${path#${android_top}/}");
    done;
  fi;

  # Automated remote changes list detection
  if [ -z "${project_paths[*]}" ]; then
    mapfile -t project_paths < <(androidprojectpaths "${project_owner}" "${project_name}");
  fi;

  # Array dimensions
  project_count=${#project_paths[@]};
  project_index=0;

  # ==========================================================
  # Repo root
  croot;

  # ==========================================================
  # Variables
  local commits_list=();
  local commits_result;
  local commits_todo;
  local git_revs_count;
  local git_revs_count_local;
  local git_revs_count_remote;
  local path_branch;
  local project_github;
  local user_is_owner='';

  # Check current user rights
  if [ "$(git config --global user.email)" = "${project_email}" ]; then
    user_is_owner='true';
  fi;

  # Projects loader
  for path in "${project_paths[@]}"; do

    # Project selection
    path=${path%/};
    path_branch=${path//\//_};
    project_index=$((project_index + 1));
    echo '';
    echo -e " \e[1;37m=== [${project_index}/${project_count}] ${path} [${path_branch}] ===\e[0m";
    echo '';
    croot;

    # Ignore missing git projects
    if [ ! -d "${path}/.git" ]; then
      echo '  .git project not found, ignoring...';
      continue;
    fi;
    cd "${path}/";

    # Targets selection
    project_github="https://github.com/${project_owner}/${project_name}.git";

    # Fetch
    if ! git fetch "${project_github}" "${path_branch}"; then
      echo 'Remote project branch not found, ignoring...';
      continue;
    fi;

    # Compare
    git_revs_count=$(git rev-list --left-right --count HEAD...FETCH_HEAD | tail -n 1);
    git_revs_count_local=$(echo "${git_revs_count:-0}" | awk '{ print $1 }');
    git_revs_count_remote=$(echo "${git_revs_count:-0}" | awk '{ print $2 }');
    echo '';
    if [ ! -z "${git_revs_count_local}" ] && [ "${git_revs_count_local}" -eq 0 ]; then
      echo -e "  \e[1;32m> Differences: ${git_revs_count:-Missing history}\e[0m";
    else
      echo -e "  \e[1;31m> Differences: ${git_revs_count:-Missing history}\e[0m";
    fi;
    echo '';

    # Handle unupdated projects
    if [ "${git_revs_count_local}" -ne 0 ] && [ "${git_revs_count_remote}" -ne 0 ]; then
      echo -e '  \e[1;37m> Commands: Y (Rebase on new changes)';
      echo -e '              n (Use the old project sources)';
      if [ ! -z "${user_is_owner}" ]; then
      echo -e '              a (Automatically update all projects)';
      fi;
      echo -e '              l (Automatically patch all projects)';
      if [ ! -z "${user_is_owner}" ]; then
        echo -e '              p (Push local changes upstream)';
      fi;
      echo '';
      echo -en '  \e[1;33m> Choice to patch and update the project ? \e[0m';
      if [ -z "${key_input}" ]; then
        read -r -t 20 key;
      else
        key=${key_input};
        echo "${key}";
      fi;
      echo '';

      # Adapt patcher to key input
      if [ -z "${key}" ]; then
        key_input=;
        key_automated_patch=y;
        key_automated_push=;
      elif [ "${key}" = 'a' ] || [ "${key}" = 'A' ]; then
        key_input=a;
        key_automated_patch=y;
        key_automated_push=y;
      elif [ "${key}" = 'n' ] || [ "${key}" = 'N' ]; then
        key_input=;
        key_automated_patch=n;
        key_automated_push=n;
      elif [ "${key}" = 'l' ] || [ "${key}" = 'L' ]; then
        key_input=l;
        key_automated_patch=y;
        key_automated_push=n;
      elif [ "${key}" = 'p' ] || [ "${key}" = 'P' ]; then
        key_input=;
        key_automated_patch=n;
        key_automated_push=y;
      elif [ "${key}" = 'y' ] || [ "${key}" = 'Y' ]; then
        key_input=;
        key_automated_patch=y;
        key_automated_push=;
      fi;

      # Rebase the tree
      if [ "${key_automated_patch}" = 'y' ]; then

        # Abort last cherry-pick
        echo '';
        git cherry-pick --abort &> /dev/null;

        # Detect commits to pick - Detect own updated project
        readarray -t commits_list <<< "$(git log --committer="${project_email}" --committer='marker.commit' --reverse --format=format:%H HEAD)";
        if [ -z "${commits_list[*]}" ]; then

          # Detect commits to pick - Detect own cherry-picked commits
          readarray -t commits_list <<< "$(git log --committer="${project_email}" --committer='marker.commit' --reverse --format=format:%H FETCH_HEAD)";

        # Detect commits to pick - Detect own cherry-picked additional commits in an own updated project
        else
          readarray -t commits_list <<< "$(git log --format=format:'%H|%ce' FETCH_HEAD | sed "s/|${project_email}//g" | sed '/|/q' | sed 's/|marker.commit//' | sed 's/|.*//' | tac)";
          if [ -z "${commits_list[*]}" ] || [ "${#commits_list[@]}" -gt "${git_revs_count_remote}" ]; then

            # Detect commits to pick - Detect own cherry-picked commits based on commits delta
            readarray -t commits_list <<< "$(git rev-list --reverse FETCH_HEAD~"${git_revs_count_remote}"..FETCH_HEAD)";

          fi;
        fi;

        # Pick all commits
        for sha1 in "${commits_list[@]}"; do
          git cherry-pick "${sha1}";
          commits_result=${?};

          # Failed update
          if [ ${commits_result} -ne 0 ] && [ ${commits_result} -ne 1 ] || ! git diff-index --ignore-submodules --quiet HEAD --; then
            commits_todo=${commits_list[*]};
            commits_todo=${commits_todo#*${sha1}};
            echo '';
            echo -e '  \e[1;31m< Automatic update failed...';
            echo "     Tried to add following commits: ${sha1} ${commits_todo}";
            echo -e '  \e[1;33m> Manually fix then push with :\e[0m';
            echo "     gitbranchpusher '${key_automated_patch}' '${project_github}' '${path_branch}';";
            echo '';
            commits_todo="git cherry-pick ${sha1} ${commits_todo}";
            ${commits_todo} 2>/dev/null;
            return 1;
          fi;

          # Detect marker commits and tag them
          if git --no-pager log -1 --pretty=format:"%ae" "${sha1}" | grep -q 'marker.commit'; then
            git commit --allow-empty --no-edit;
            gitcamarker;
          fi;

        done;

        # Automatic push
        if [ ! -z "${user_is_owner}" ]; then
          gitbranchpusher "${key_automated_push}" "${project_github}" "${path_branch}";
        fi;

      # Only push the project
      elif [ "${key_automated_push}" = 'y' ]; then

        # Automatic push
        if [ ! -z "${user_is_owner}" ]; then
          gitbranchpusher "${key_automated_push}" "${project_github}" "${path_branch}";
        fi;

      # Project is ready
      else
        git reset --hard FETCH_HEAD;
        git stash -u;
      fi;

    # Handle unupdated remotes
    elif [ "${git_revs_count_local}" -ne 0 ]; then

      # Automatic push
      if [ ! -z "${user_is_owner}" ]; then
        gitbranchpusher "${key_automated_push}" "${project_github}" "${path_branch}";
      fi;

    # Project is ready
    else
      git reset --hard FETCH_HEAD;
      git stash -u;
    fi;

  done;

  # ==========================================================
  # Return to current folder
  echo '';
  cd "${currentdir}";
  return 0;
}

# === Android Project Rebaser ===
function androidprojectrebaser()
{
  # Usage
  if [ -z "${1}" ] || [ -z "${3}" ]; then
    echo '';
    echo ' Usage: androidprojectrebaser <owner> <project_branch> <"project_paths::name::upstream::branch"> [specific_path] (Android project rebaser)';
    echo '';
    return;
  fi;

  # Variables
  local projects_owner=${1};
  local projects_branch=${2};
  local projects_paths=${3};
  local project_specific_inputs=("${@:4}");
  local android_top;
  local currentdir=${PWD};
  local projects_specific=;

  # Automated key shortcut
  local key_automated=;
  if [[ "${projects_specific}" == 'y' ]] || [[ "${projects_specific}" == 'n' ]]; then
    key_automated=${projects_specific};
    projects_specific=;
  fi;

  # ==========================================================
  # Extract specific projects paths
  if [ ! -z "${project_specific_inputs[*]}" ]; then
    android_top=$(gettop);
    for path in "${project_specific_inputs[@]}"; do
      path=$(readlink -f "${path}");
      projects_specific="${projects_specific} ${path#${android_top}/}";
    done;
  fi;

  # Repo root
  if [ -z "$(type -t croot)" ]; then
    while [ ! -e './build/envsetup.sh' ]; do
      cd ../;
    done;
    source ./build/envsetup.sh;
  fi;
  croot;

  # ==========================================================
  # Variables
  local commits_result;
  local git_revs_count;
  local git_revs_count_remote;
  local project_branch;
  local project_name;
  local project_path;
  local project_upstream;
  local push_branch=${projects_branch};
  local push_command;

  # Projects loader
  for project in ${projects_paths}; do

    # Project string parsing
    project_path=${project%%::*};
    project_name=${project#*::};
    project_upstream=${project_name#*::};
    project_branch=${project_upstream#*::};
    project_name=${project_name%%::*};
    project_upstream=${project_upstream%%::*};

    # Jump to specific project
    if [ ! -z "${projects_specific}" ]; then
      if [[ ! "${projects_specific}" == *"${project_path%/}"* ]]; then
        continue;
      fi;
    fi;

    # Append upstream project name if needed
    if [ "${project_upstream: -1}" = '/' ]; then
      project_upstream=${project_upstream}${project_name};
    fi;

    # Without a branch as input, align with upstream
    if [ -z "${projects_branch}" ]; then
      push_branch=${project_branch};

    # Without a branch on project, use branch input
    elif [ -z "${project_branch}" ]; then
      project_branch=${projects_branch};
      push_branch=${projects_branch};
    fi;

    # Project introduction
    echo '';
    echo -e " \e[1;37m=== ${project_path} [${project_upstream} @${project_branch}] ===\e[0m";
    echo '';
    croot;

    # Ignore non-git projects
    if [ ! -d "${project_path}/.git" ]; then
      echo '  .git project not found, ignoring...';
      continue;
    fi;
    cd "${project_path}/";

    # Prepend GitHub is needed
    if [[ ! "${project_upstream}" = *'://'* ]]; then
      project_upstream="https://github.com/${project_upstream}";
    fi;

    # Fetch and unshallow if needed
    git fetch --unshallow "https://github.com/${projects_owner}/${project_name}" "${push_branch}" 2>&1 \
        | grep -v 'on a complete repository does not make sense';

    # Fetch and compare
    git fetch "${project_upstream}" "${project_branch}";
    git_revs_count=$(git rev-list --left-right --count HEAD...FETCH_HEAD | tail -n 1);
    git_revs_count_remote=$(echo "${git_revs_count}" | awk '{ print $2 }');
    echo '';
    if [ "${git_revs_count_remote}" -eq 0 ]; then
      echo -e "  \e[1;32m> Differences: ${git_revs_count:-Missing history}\e[0m";
    else
      echo -e "  \e[1;31m> Differences: ${git_revs_count:-Missing history}\e[0m";
    fi;
    echo '';

    # Handle unupdated projects
    if [ "${git_revs_count_remote}" -ne 0 ]; then
      echo -e '  \e[1;37m> Commands: Y (Rebase on new changes)';
      echo -e '              n (Keep the current project sources)';
      echo -e '              a (Automatically rebase all projects)';
      echo -e '              l (Only preview all differences)';
      echo '';
      echo -en '  \e[1;33m> Choice to rebase the project ? \e[0m';
      if [ -z "${key_automated}" ]; then
        read -r -t 20 key;
        if [ "${key}" = 'a' ] || [ "${key}" = 'A' ]; then
          key_automated=y;
        elif [ "${key}" = 'l' ] || [ "${key}" = 'L' ]; then
          key=n;
          key_automated=n;
        fi;
      else
        key=${key_automated};
        echo "${key}";
      fi;

      # Rebase the tree
      if [ ! "${key}" = 'n' ] && [ ! "${key}" = 'N' ]; then
        echo '';
        git rebase --abort;
        git rebase FETCH_HEAD;
        commits_result=${?};

        # Build push command
        push_command="git push -f https://github.com/${projects_owner}/${project_name} HEAD:refs/heads/${push_branch}";

        # Failed rebase
        if [ ${commits_result} -ne 0 ] || ! git diff-index --ignore-submodules --quiet HEAD --; then
          echo '';
          echo -e '  \e[1;31m< Automatic rebase failed...';
          echo -e '  \e[1;33m> Manually fix then push with :\e[0m';
          echo "     ${push_command};";
          echo '';
          return 1;
        fi;

        # Push the tree
        push_command="git push -f https://github.com/${projects_owner}/${project_name} HEAD:refs/heads/${push_branch}";
        echo '';
        echo -en "  \e[1;33m> androidprojectrebaser: ${push_command} [y/a/N] ?\e[0m ";
        notify-send "androidprojectrebaser: Push ${project_path} ?" 2> /dev/null;
        if [ -z "${key_automated}" ]; then
          read -r -t 20 key;
          if [ "${key}" = 'a' ] || [ "${key}" = 'A' ]; then
            key=y;
            key_automated=y;
          fi;
        else
          key=${key_automated};
          echo "${key}";
        fi;

        # Push the tree
        echo '';
        if [[ "${key}" == 'y' ]] || [[ "${key}" == 'Y' ]]; then
          ${push_command};
        elif [ "${key}" = 'n' ] || [ "${key}" = 'N' ]; then
          echo '  < Upload ignored. Consider uploading later';
        else
          echo '  < Upload ignored after timeout... Consider uploading later';
        fi;

      fi;
    fi;

  done;

  # ==========================================================
  # Return to current folder
  echo '';
  cd "${currentdir}";
}

# === Android Project For Each Path ===
function androidprojectforeach()
{
  # Usage
  if [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ]; then
    echo '';
    echo ' Usage: androidprojectforeach <owner> <project_name> <"commands"> (Android project paths commands runner)';
    echo '';
    return;
  fi;

  # Variables
  local path;

  # Android project remote paths list
  for path in $(androidprojectpaths "${1}" "${2}"); do

    # Access path
    croot;
    cd "${path}";

    # Information
    echo '';
    echo "[${path}]";

    # Run commands
    ${3};

  done;

  # Return to repo root
  croot;
  echo '';
}

# === Android Project Sync ===
function androidprojectsync()
{
  # Usage
  if [ -z "${1}" ] || [ -z "${2}" ]; then
    echo '';
    echo ' Usage: androidprojectsync <owner> <project_name> (Android project paths repo syncer)';
    echo '';
    return;
  fi;

  # Variables
  local branch;
  local path;

  # Android project remote paths list
  for path in $(androidprojectpaths "${1}" "${2}"); do

    # Access path
    croot;
    cd "${path}";

    # Information
    echo '';
    echo " androidproject: Syncing '${path}' path...";

    # Fetch and reset
    branch=$(repogetbranch);
    git fetch "$(gitgetremote "${branch}")" "${branch}" 2>&1;
    git reset --hard FETCH_HEAD;

  done;

  # Return to repo root
  croot;
  echo '';
}

# === Android Project Ungraft ===
function androidprojectungraft()
{
  # Usage
  if [ -z "${1}" ] || [ -z "${2}" ]; then
    echo '';
    echo ' Usage: androidprojectungraft <owner> <project_name> (Android project paths ungrafter)';
    echo '';
    return;
  fi;

  # Variables
  local branch;
  local path;

  # Android project remote paths list
  for path in $(androidprojectpaths "${1}" "${2}"); do

    # Access path
    croot;
    cd "${path}" 2>/dev/null || continue;

    # Information
    echo '';
    echo " androidprojectungraft: Fetching '${path}' last 100 commits...";

    # Fetch and ungraft
    branch=$(repogetbranch);
    git fetch --depth 100 "$(gitgetremote "${branch}")" "${branch}";

  done;

  # Return to repo root
  croot;
  echo '';
}

# === Android Project Unshallow ===
function androidprojectunshallow()
{
  # Usage
  if [ -z "${1}" ] || [ -z "${2}" ]; then
    echo '';
    echo ' Usage: androidprojectunshallow <owner> <project_name> (Android project paths unshallower)';
    echo '';
    return;
  fi;

  # Variables
  local branch;
  local path;

  # Android project remote paths list
  for path in $(androidprojectpaths "${1}" "${2}"); do

    # Access path
    croot;
    cd "${path}" 2>/dev/null || continue;

    # Information
    echo '';
    echo " androidprojectunshallow: Checking '${path}' shallow status...";

    # Fetch and unshallow
    branch=$(repogetbranch);
    git fetch --unshallow "$(gitgetremote "${branch}")" "${branch}" 2>&1 \
	    | grep -v 'on a complete repository does not make sense';

  done;

  # Return to repo root
  croot;
  echo '';
}
